Outputs

This week's focus was on using knowledge of PCB fabrication and programming to utilize various output devices for achieving the goals. Among the options explored were LEDs, Neopixel LEDs, displays, DC motors, and more. I opted to work with an OLED display called "OLED Display SSD1306." Stay tuned to see the outcome of this week's assignment.

Tetris

PCB

As the name implies, I made it possible to play Tetris with the board I created. Utilizing the PCB developed in the previous week, I ensured it contained all the necessary components or this particular game.

This PCB incorporates the XIAO ESP32C3 as the microprocessor, along with 5 pushbuttons equipped with their respective pulldown resistors, an OLED SSD1306, and a buzzer. I've already soldered and tested this particular PCB in the previous week, and this week's focus revolves around utilizing this design for the Tetris game. For further clarification, you can refer to my Week 8 submission.

The key component for this assignment is the widely-used OLED (Organic Light Emitting Diode) SSD1306 display. It boasts simplicity combined with remarkable versatility, characterized by several outstanding features:

  1. Interface: This display typically uses I2C to communicate with a microcontroller and it also have SPI communication, both of these methods make it easy to connect.
  2. Resolution: The SSD1306 has a resolution of 128x64 pixels, although it can be adjusted for any kind of resolution between this range.
  3. Monochromatic Display: While OLED displays come in various colors, they typically remain monochromatic, displaying only one color (often white, blue, or yellow) against a black background. Each pixel is individually controllable, allowing for creative possibilities where the entire screen becomes your canvas.
  4. Low Power Consumption: The OLED SSD1306 display boasts advanced technology compared to traditional LCD displays, as it does not require a backlight, resulting in lower power consumption. Additionally, this display offers power-saving modes, enhancing its versatility even further.
  5. Internal RAM Buffer: The SSD1306 incorporates and internal buffer, this allows for smooth updates of the display. This buffer can be amnipulated bu the microcontroller to update specific parts without refresing the entire screen.

Code

Now that part that matters, this is a very large code so I am going to explain the major changes so it can be more undestandable, before I start this code is not mine, so i will proportion the link to his webpage.


#include <‍Wire.h>
#include <‍Adafruit_GFX.h>
#include <‍Adafruit_SSD1306.h>
				

These lines include the libraries used in this project.


#define WIDTH 64
#define HEIGHT 128
				

These lines define the width and the height of the OLED display.

Adafruit_SSD1306 display(128, 64, &Wire, -1);

This single line initialized the OLED display using the Wire library for communication, and a reset pin of -1 (wich means it's not used).


const char pieces_S_l[2][2][4] = {{
	{0, 0, 1, 1}, {0, 1, 1, 2}
},
},
	{0, 1, 1, 2}, {1, 1, 0, 0}
}};
				

This is a 3D array that define the shape of a tetris piece. Spcifically, this array represents the "S" type when is rotated to the left.


const char pieces_S_r[2][2][4] = {{
	{1, 1, 0, 0}, {0, 1, 1, 2}
},
},
	{0, 1, 1, 2}, {0, 0, 1, 1}
}};
				

This 3D array represents the "S" type when rotated to the right


const char pieces_L_l[4][2][4] = {{
	{0, 0, 0, 1}, {0, 1, 2, 2}
},
},
	{0, 1, 2, 2}, {1, 1, 1, 0}
},
},
	{0, 1, 1, 1}, {0, 0, 1, 2}
},
},
	{0, 0, 1, 2}, {1, 0, 0, 0}
}};
				

This 3D array represents the "L" shape when rotated to the left.


const char pieces_Sq[1][2][4] = {{
	{0, 1, 0, 1}, {0, 0, 1, 1}
}};
				

This 3D array represents the "squared" shape. It only need one rotation.


const char pieces_T[4][2][4] = {{
	{0, 0, 1, 0}, {0, 1, 1, 2}
},
},
	{0, 1, 1, 2}, {1, 0, 1, 1}
},
},
	{1, 0, 1, 1}, {0, 1, 1, 2}
},
},
	{0, 1, 1, 2}, {0, 0, 1, 0}
}};
				

This 3D array represents the "T" shape when rotated in each of the four possible directions.


const char pieces_l[2][2][4] = {{
	{0, 1, 2, 3}, {0, 0, 0, 0}
},
},
	{0, 0, 0, 0}, {0, 1, 2, 3}
}};
				

This 3D array represents the "line" shape, including two rotations.


const short MARGIN_TOP = 19; - declares a constant variable MARGIN_TOP with a value of 19 of type short.
const short MARGIN_LEFT = 3; - declares a constant variable MARGIN_LEFT with a value of 3 of type short.
const short SIZE = 5; - declares a constant variable SIZE with a value of 5 of type short.
const short TYPES = 6; - declares a constant variable TYPES with a value of 6 of type short.
#define SPEAKER_PIN 3 - creates a macro with the name SPEAKER_PIN and a value of 3. This allows the code to refer to SPEAKER_PIN throughout the rest of the code and substitute it with the value 3.
const int MELODY_LENGTH = 10; - declares a constant variable MELODY_LENGTH with a value of 10 of type int.
const int MELODY_NOTES[MELODY_LENGTH] = {262, 294, 330, 262}; - declares an array MELODY_NOTES of size MELODY_LENGTH (which is 10), and initializes it with four integer values.
const int MELODY_DURATIONS[MELODY_LENGTH] = {500, 500, 500, 500}; - declares an array MELODY_DURATIONS of size MELODY_LENGTH (which is 10), and initializes it with four integer values.
int click[] = { 1047 }; - declares an array click of size 1, and initializes it with one integer value.
int click_duration[] = { 100 }; - declares an array click_duration of size 1, and initializes it with one integer value.
int erase[] = { 2093 }; - declares an array erase of size 1, and initializes it with one integer value.
int erase_duration[] = { 100 }; - declares an array erase_duration of size 1, and initializes it with one integer value.
word currentType, nextType, rotation; - declares three variables of type word, named currentType, nextType, and rotation.
short pieceX, pieceY; - declares two variables of type short, named pieceX and pieceY.
short piece[2][4]; - declares a two-dimensional array piece of size 2x4 with elements of type short.
int interval = 20, score; - declares two variables, interval and score, of type int. interval is initialized with a value of 20.
long timer, delayer; - declares two variables, timer and delayer, of type long.
boolean grid[10][18]; - declares a two-dimensional array grid of size 10x18 with elements of type boolean.
boolean b1, b2, b3; - declares three variables of type boolean, named b1, b2, and b3.
int left=D9;
int right=D7;
int change=D6;
int speed=D10;
				

These lines define 4 intefer variables left, right, change and speed, so with this it makes the game playable. I made adjustments to the default pin configurations in the code to ensure compatibility with the Xiao ESP32C3 board and the specific button placeholders I selected.


void checkLines(){
	boolean full;
	for(short y = 17; y >= 0; y--){
		full = true;
		for(short x = 0; x < 10; x++){
			full = full && grid[x][y];
		}
		if(full){
			breakLine(y);
			y++;
		}
	}
}
void breakLine(short line){
	tone(SPEAKER_PIN, erase[0], 1000 / erase_duration[0]); 
	delay(100);
	noTone(SPEAKER_PIN);
	for(short y = line; y >= 0; y--){
		for(short x = 0; x < 10; x++){
			grid[x][y] = grid[x][y-1];
		}
	}
	for(short x = 0; x < 10; x++){
		grid[x][0] = 0;
	}
	display.invertDisplay(true);
	delay(50);
	display.invertDisplay(false);
	score += 10;
}
				

This code implements the functions checkLines() and breakLines() for clearing out completed lines in the game grid.

checkLines() Iterates through each row of the grid from bottom to the top, by checking if a linea is full or not. If it is, it calls the breakLine() function, passing the line number as a parameter, to clear that line.

breakLine() It first plays an "erase" tone using the tone() function on the SPEAKER_PIN, indicating that a lina has been cleared. It then shift down all the rows above the cleared line by one cell and clears the top row. It also adds 10 to the score for clearing a line by one cell and celars the top row. It also adds 10 to the score for clearing a line. Lastly, it inverts the display of the LED matris for 50ms to create a visual efect of the cleared line, and thensets the display back to normal.


void refresh(){
	display.clearDisplay();
	drawLayout();
	drawGrid();
	drawPiece(currentType, 0, pieceX, pieceY);
	display.display();
}
void drawGrid(){
	for(short x = 0; x < 10; x++)
		for(short y = 0; y < 18; y++)
			if(grid[x][y])
			display.fillRect(MARGIN_LEFT + (SIZE + 1)*x, MARGIN_TOP + (SIZE + 1)*y, SIZE, SIZE, WHITE);
}
boolean nextHorizontalCollision(short piece[2][4], int amount){
	for(short i = 0; i < 4; i++){
		short newX = pieceX + piece[0][i] + amount;
		if(newX > 9 || newX < 0 || grid[newX][pieceY + piece[1][i]])
			return true;
	}
	return false;
}
boolean nextCollision(){
	for(short i = 0; i < 4; i++){
		short y = pieceY + piece[1][i] + 1;
		short x = pieceX + piece[0][i];
		if(y > 17 || grid[x][y])
			return true;
	}
	return false;
}
				

refresh() This function clears the display, draws the layout, dras the grid and finally draws the current piece of the display.

drawGrid() This funciton iterates over the entire grid and if a cell is filled, it draws a white rectangle with the size of the cell.

nextHorizontalCollision() This function checks if there is any collision with the grid on the horizontal direction. It does this by checking each cell in the current piece and adding an amount to its x position. If the new x position is outside of the grid or is already occupied, then there is a collision and the function return true.

nextCollision() This function checks if there is any collision with the grid on the vertical direction. It does this by checking each in the current piece and adding one to its Y position. If the new Y position is outside of the gri or is already occupied, then there is a collision and the function return true.


void generate(){
	currentType = nextType;
	nextType = random(TYPES);
	if(currentType != 5)
		pieceX = random(9);
	else
		pieceX = random(7);
	pieceY = 0;
	rotation = 0;
	copyPiece(piece, currentType, rotation);
}
void drawPiece(short type, short rotation, short x, short y){
	for(short i = 0; i < 4; i++)
		display.fillRect(MARGIN_LEFT + (SIZE + 1)*(x + piece[0][i]), MARGIN_TOP + (SIZE + 1)*(y + piece[1][i]), SIZE, SIZE, WHITE);
}
void drawNextPiece(){
	short nPiece[2][4];
	copyPiece(nPiece, nextType, 0);
	for(short i = 0; i < 4; i++)
		display.fillRect(50 + 3*nPiece[0][i], 4 + 3*nPiece[1][i], 2, 2, WHITE);
}
				

generate() This function set the variables for the next Tetris piece that will be dropped onto the game board. It sets the currentType to be the same as the nextType, and then generates a new nextType using the random() function.

Then the copyPiece() function is called to copy the appropiate piece into the piece array.

drawPiece() This function takes in the type of a Tetris piece, its rotation, and its current X and Y coordinates on the game board. It then uses a for loop to loop through the four block of the piece and draw each block on to the game board using the display.fillRect() function.

drawNextPiece() This function draws the next Tetris piece in the preview box on the right side of the game board. It first copies the next piece into the nPiece array using the copyPiece() function. It then loop through the four block of the piece and draws each block onto the preview box using the display.fillRect() function.


void copyPiece(short piece[2][4], short type, short rotation){
	switch(type){
	case 0: //L_l
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_L_l[rotation][0][i];
			piece[1][i] = pieces_L_l[rotation][1][i];
		}
		break;
	case 1: //S_l
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_S_l[rotation][0][i];
			piece[1][i] = pieces_S_l[rotation][1][i];
		}
		break;
	case 2: //S_r
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_S_r[rotation][0][i];
			piece[1][i] = pieces_S_r[rotation][1][i];
		}
		break;
	case 3: //Sq
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_Sq[0][0][i];
			piece[1][i] = pieces_Sq[0][1][i];
		}
		break;
		case 4: //T
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_T[rotation][0][i];
			piece[1][i] = pieces_T[rotation][1][i];
		}
		break;
		case 5: //l
		for(short i = 0; i < 4; i++){
			piece[0][i] = pieces_l[rotation][0][i];
			piece[1][i] = pieces_l[rotation][1][i];
		}
		break;
	}
}
short getMaxRotation(short type){
	if(type == 1 || type == 2 || type == 5)
		return 2;
	else if(type == 0 || type == 4)
		return 4;
	else if(type == 3)
		return 1;
	else
		return 0;
}
boolean canRotate(short rotation){
	short piece[2][4];
	copyPiece(piece, currentType, rotation);
	return !nextHorizontalCollision(piece, 0);
}
				

getMaxRotation() This function returns the maximum number of rotations that a given piece type can have. It returns 2 for typer 1,2, and 5,4 for typer 0 and 4,1 for type 3, and 0 for any other type.

canRotate() This function checks if the current piece can be rotated by rotation amount. It does this by calling copyPiece() to create a new piece array with the rotated blocks, and then callingn nextHorizontalCollision() to check if the rotated piece would overlap with any fallen blocks.


void drawLayout(){
	display.drawLine(0, 15, WIDTH, 15, WHITE);
	display.drawRect(0, 0, WIDTH, HEIGHT, WHITE);
	drawNextPiece();
	char text[6];
	itoa(score, text, 10);
	drawText(text, getNumberLength(score), 7, 4);
}
short getNumberLength(int n){
	short counter = 1;
	while(n >= 10){
		n /= 10;
		counter++;
	}
	return counter;
}
void drawText(char text[], short length, int x, int y){
	display.setTextSize(1);      // Normal 1:1 pixel scale
	display.setTextColor(WHITE); // Draw white text
	display.setCursor(x, y);     // Start at top-left corner
	display.cp437(true);         // Use full 256 char 'Code Page 437' font
	for(short i = 0; i < length; i++)
		display.write(text[i]);
}
				

drawLayout() This function is responsible for drawing the basic layout of the game screen, including the border, the separator line, the score, and the next piece. It calls the drawNextPiece() and drawText() functions to draw the next piece and the score, respectively.

getNumberLength() This function is a helper function that takes an integer as input and returns the number of digits in it. This is useful for determining the length of the score that needs to be drawn.

drawText() This function draws a string of text on the screen at the given x and y coordinates. It takes in the text to be drawn, its length, and the x and y coordinates as parameters. This function sets the text size, color, cursor position, and font before drawing the text.


void setup() {
	pinMode(left, INPUT_PULLUP);
	pinMode(right, INPUT_PULLUP);
	pinMode(change, INPUT_PULLUP);
	pinMode(speed, INPUT_PULLUP);
	pinMode(SPEAKER_PIN, OUTPUT);
	Serial.begin(9600);
	// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
	if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
		Serial.println(F("SSD1306 allocation failed"));
		for(;;); // Don't proceed, loop forever
	}
	display.setRotation(1);
	display.clearDisplay();
	display.drawBitmap(3, 23, mantex_logo, 64, 82,  WHITE);
	display.display();
	delay(2000);
	display.clearDisplay();
	drawLayout();
	display.display();
	randomSeed(analogRead(0));
	nextType = random(TYPES);
	generate();
	timer = millis();
}
				

setup() This function is a required function in an Arduino sketch that runs once at the beginning of the program. In this particular sketch, it initializes the input and output pins, sets up communication through Serial, initializes and clears the OLED display, and sets the rotation of the display. It then displays a logo for two seconds before clearing the display and calling the drawLayout() function to draw the game layout. Finally, it sets the random seed, generates the first piece, and starts the timer.


void loop() {
if(millis() - timer > interval){
	checkLines();
	refresh();
	if(nextCollision()){
		for(short i = 0; i < 4; i++)
		grid[pieceX + piece[0][i]][pieceY + piece[1][i]] = 1;
		generate();
	}else
		pieceY++;
	timer = millis();
	}
		if(!digitalRead(left)){
		tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]);
		delay(100);
		noTone(SPEAKER_PIN);
		if(b1){
			if(!nextHorizontalCollision(piece, -1)){
				pieceX--;
				refresh();
			}
			b1 = false;
		}
	}else{
		b1 = true;
	}
	if(!digitalRead(right)){
		tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]);
		delay(100);
		noTone(SPEAKER_PIN);
		if(b2){
		if(!nextHorizontalCollision(piece, 1)){
			pieceX++;
			refresh();
		}
		b2 = false;
		}
	}else{
		b2 = true;
	}
	if(!digitalRead(speed)){
		interval = 20;
	} else{
		interval = 400;
	}
	if(!digitalRead(change)){
		tone(SPEAKER_PIN, click[0], 1000 / click_duration[0]);
		delay(100);
		noTone(SPEAKER_PIN);
		if(b3){
			if(rotation == getMaxRotation(currentType) - 1 && canRotate(0)){
				rotation = 0;
		}else if(canRotate(rotation + 1)){
			rotation++;
		}  
		copyPiece(piece, currentType, rotation);
		refresh();
		b3 = false;
		delayer = millis();
	}
	}else if(millis() - delayer > 50){
	b3 = true;
	}
}
				

loop() This function is the main function in the Arduino sketch that is executed repeatedly after the setup() function is called.

Final product

After designing and fabricating the PCB, soldering all the respective components onto it, and finally programming and compiling the code, the next step is to upload it to the board and test its functionality.

It seems there might be a delay with the buttons and an issue with the buzzer's sound quality, possibly due to interference or a malfunction. Further testing is required to pinpoint the exact problem. If you're interested in the original project, you can find by clicking here.

Group assignment

This week's assignment focused on measuring the power consumption of output devices, specifically three motors: a Pololu 125:1 Metal Gearmotor 20Dx44L mm 6V CB, a Handson Technology 3420 Dual Ball Bearing Long Life DC Motor, and a TowerPro MG995 Servo Motor. It was very interesting to observe how the measurements varied between the different motors. For more details about this week's assignment, you can check out the output devices week or by clicking here.

Useful links

Files